package org.test4j.mock.faking.meta;

import g_asm.org.objectweb.asm.Type;
import org.test4j.mock.faking.util.ClassLoad;
import org.test4j.mock.faking.util.TypeUtility;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static org.test4j.mock.faking.meta.MethodId.getDesc;
import static org.test4j.mock.faking.meta.MethodId.getName;

public class FakeMethods {
    /**
     * mock行为序号
     */
    public final long fakeSeqNo;
    /**
     * mock方法列表
     */
    private final List<FakeMethod> methods = new ArrayList<>();

    public FakeMethods(long fakeSeqNo) {
        this.fakeSeqNo = fakeSeqNo;
    }

    public FakeMethod findMethod(MethodId methodId) {
        FakeMethod compatible = null;
        for (FakeMethod method : this.methods) {
            if (!isClassMatch(methodId, method.meta)) {
                continue;
            }
            if (Objects.equals(method.meta.methodDesc, methodId.methodDesc)) {
                return method;
            }
            if (this.isCompatible(method.meta.methodDesc, methodId.methodDesc)) {
                compatible = method;
            }
        }
        return compatible;
    }

    /**
     * 如果是构造函数, 必须是本类
     * 如果是普通函数, 必须是本类或子类
     *
     * @param real
     * @param fake
     * @return
     */
    private boolean isClassMatch(MethodId real, MethodId fake) {
        if (Objects.equals(real.declaredClassDesc, fake.declaredClassDesc) || fake.declaredClassDesc == null) {
            return TypeUtility.isAssignable(fake.realClass(), real.realClass());
        } else {
            return false;
        }
    }

    /**
     * 实际声明的方法和fake中定义的方法是否兼容
     *
     * @param fakeDesc
     * @param declaredDesc
     * @return
     */
    private boolean isCompatible(String fakeDesc, String declaredDesc) {
        if (!Objects.equals(getName(fakeDesc), getName(declaredDesc))) {
            return false;
        }
        Type[] fakeTypes = Type.getArgumentTypes(getDesc(fakeDesc));
        Type[] declaredTypes = Type.getArgumentTypes(getDesc(declaredDesc));
        if (fakeTypes.length != declaredTypes.length) {
            return false;
        }
        for (int index = 0; index < fakeTypes.length; index++) {
            String fakeTypeName = fakeTypes[index].getClassName();
            String declareTypeName = declaredTypes[index].getClassName();
            if (Objects.equals(fakeTypeName, declareTypeName)) {
                continue;
            }
            Class fakeType = ClassLoad.loadClass(fakeTypeName);
            Class declaredType = ClassLoad.loadClass(declareTypeName);
            if (!TypeUtility.isAssignable(declaredType, fakeType)) {
                return false;
            }
        }
        return true;
    }

    public void clear() {
        this.methods.clear();
    }

    public void add(FakeMethod fakeMethod) {
        this.methods.add(fakeMethod);
    }
}